<!--
 * @Description  : 
 * @Version      : 1.0
 * @Author       : seraph
 * @Date         : 2024-12-26 12:19:10
 * @LastEditors  : seraph
 * @LastEditTime : 2025-02-18 16:57:00
-->
<template>
  <h3>Class 与 Style 绑定</h3>
  <p>
    数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 class 和 style 都是
    attribute，我们可以和其他 attribute 一样使用 v-bind
    将它们和动态的字符串绑定。但是，在处理比较复杂的绑定时，通过拼接生成字符串是麻烦且易出错的。因此，Vue
    专门为 class 和 style 的 v-bind 用法提供了特
    殊的功能增强。除了字符串外，表达式的值也可以是对象或数组。
  </p>
  <hr />
  <h3>绑定 HTML class</h3>
  <h4>绑定对象</h4>
  <p>我们可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class：</p>
  <pre><code class="html" v-html="highlighted(rawCode1, 'html')"></code></pre>
  <pre><code class="javascript">const isActive = ref(true)</code></pre>
  <pre><code class="html">// .active 样式，颜色显示为红色
      .active {
      color: red;
      }</code></pre>
  <p>
    <span :class="{ active: isActive }" ref="HTMLElement1">渲染后的效果</span>
  </p>
  <p>
    <span>渲染出的 HTML 代码：{{ renderingHtml1.toString() }}</span>
  </p>

  <button @click="isActive = !isActive">toggle isActive: {{ isActive }}</button>
  <p>上面的语法表示样式 active 是否存在，取决于数据属性 isActive 的真假值。</p>
  <br>

  <p>
    你可以在对象中写多个字段来操作多个 class。此外，:class 指令也可以和一般的 class attribute
    共存。举例来说，下面这样的状态：
  </p>
  <pre><code class="javascript" v-html="highlighted(rawCode2, 'javascript')"></code></pre>
  <p>配合以下模板及 css 样式：</p>
  <pre><code class="html" v-html="highlighted(rawCode3, 'html')"></code></pre>
  <pre><code class="html" v-html="highlighted(rawCode4, 'html')"></code></pre>
  <p>
    <span class="static" :class="{ active: isActive, 'text-danger': hasError }" ref="HTMLElement3">渲染后的效果</span>
  </p>
  <p>
    <span>渲染出的 HTML 代码：{{ renderingHtml3.toString() }}</span>
  </p>
  <button @click="isActive = !isActive">toogle isActive: {{ isActive }}</button>
  <button @click="hasError = !hasError">toggle hasError: {{ hasError }}</button>
  <p>
    默认值时：isActive 为真，active（字体为红色）生效；hasError
    为假，text-danger（斜体）不生效。
  </p>
  <br />

  <p>绑定的对象并不一定需要写成内联字面量的形式，也可以直接绑定一个对象：</p>
  <pre><code class="javascript" v-html="highlighted(rawCode5, 'javascript')"></code></pre>
  <pre><code class="html" v-html="highlighted(rawCode6, 'html')"></code></pre>
  <p>
    <span :class="classObject1" ref="HTMLElement6">渲染后的效果</span>
  </p>
  <p>
    <span>渲染出的 HTML 代码：{{ renderingHtml6.toString() }}</span>
  </p>

  <button @click="classObject1.active = !classObject1.active">toogle active: {{ classObject1.active }}</button>
  <button @click="classObject1['text-danger'] = !classObject1['text-danger']">toggle text-danger:
    {{ classObject1['text-danger'] }}</button>
  <p>默认 active 为真（字体为红色生效）；text-danger 为假（斜体不生效）</p>
  <br />

  <p>我们也可以绑定一个返回对象的计算属性，这是一个常用且强大的模式：</p>
  <pre><code class="javascript" v-html="highlighted(rawCode7, 'javascript')"></code></pre>
  <pre><code class="html" v-html="highlighted(rawCode8, 'html')"></code></pre>
  <p>
    <span :class="classObject2" ref="HTMLElement8">渲染后的效果</span>
  </p>
  <p>
    <span>渲染出的 HTML 代码：{{ renderingHtml8.toString() }}</span>
  </p>

  <button @click="toggelError1">toogle error: {{ error1 }}</button>
  <p>当 error1 为 null 时（默认值），!error.value 为真，active（红色）生效，text-danger（斜体）不生效。</p>
  <p>当 error1 为 { type: fatal} 时，!error.value 为假，active（红色）不生效，text-danger（斜体）生效。</p>
  <br />

  <h4>绑定数组</h4>
  <p>我们可以给 :class 绑定一个数组来渲染多个 CSS class：</p>
  <pre><code class="javascript" v-html="highlighted(rawCode9, 'javascript')"></code></pre>
  <pre><code class="html" v-html="highlighted(rawCode10, 'javascript')"></code></pre>
  <p>
    <span ref="HTMLElement10" :class="[activeClass, errorClass]">渲染后的效果</span>
  </p>
  <p>
    <span>渲染出的 HTML 代码：{{ renderingHtml10.toString() }}</span>
  </p>

  <br>

  <p>如果你也想在数组中有条件地渲染某个 class，你可以使用三元表达式：</p>
  <pre><code class="html" v-html="highlighted(rawCode11, 'html')"></code></pre>
  <p>
    <span ref="HTMLElement11" :class="[isActive ? activeClass : '', errorClass]">渲染后的效果</span>
  </p>
  <p>
    <span>渲染出的 HTML 代码：{{ renderingHtml11.toString() }}</span>
  </p>
  <p>errorClass 会一直存在，但 activeClass （默认为真）只会在 isActive 为真时才存在。</p>
  <br />

  <p>然而，这可能在有多个依赖条件的 class 时会有些冗长。因此也可以在数组中嵌套对象：</p>
  <pre><code class="html" v-html="highlighted(rawCode12, 'html')"></code></pre>
  <p>
    <span ref="HTMLElement12" :class="[{ [activeClass]: isActive }, errorClass]">渲染后的效果</span>
  </p>
  <p>
    <span>渲染出的 HTML 代码：{{ renderingHtml12.toString() }}</span>
  </p>
  <br />

  <h4>在组件上使用</h4>
  <p>对于只有一个根元素的组件，当你使用了 class attribute 时，这些 class 会被添加到根元素上并与该元素上已有的 class 合并。</p>
  <p>举例来说，如果你声明了一个组件名叫 MyComponent，模板如下：</p>
  <pre><code class="html" v-html="highlighted(rawCode13, 'html')"></code></pre>

  <p>在使用时添加一些 class：</p>
  <pre><code class="html" v-html="highlighted(rawCode14, 'html')"></code></pre>
  <p>渲染出的 HTML 为：</p>
  <pre><code class="html" v-html="highlighted(rawCode15, 'html')"></code></pre>
  <MyComponent1 class="baz boo" />
  <p>Class 的绑定也是同样的：</p>
  <pre><code class="html" v-html="highlighted(rawCode16, 'html')"></code></pre>
  <p>当 isActive 为真时，被渲染的 HTML 会是：</p>
  <pre><code class="html" v-html="highlighted(rawCode17, 'html')"></code></pre>
  <p>如果你的组件有多个根元素，你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs 属性来指定接收的元素：</p>
  <pre><code class="html" v-html="highlighted(rawCode18, 'html')"></code></pre>
  <pre><code class="html" v-html="highlighted(rawCode19, 'html')"></code></pre>
  <p>渲染的 HTML 为：</p>
  <pre><code class="html" v-html="highlighted(rawCode20, 'html')"></code></pre>
  <p>你可以在透传 <a href="https://cn.vuejs.org/guide/components/attrs.html" target="_blank">透传 Attribute</a> 一章中了解更多组件的
    attribute 继承的细节。</p>
</template>

<script lang="ts" setup>
import { ref, reactive, onMounted, computed, nextTick, watch, Ref } from 'vue'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
import javascript from 'highlight.js/lib/languages/javascript'
import html from 'highlight.js/lib/languages/xml'
import { isatty } from 'tty'
import { on } from 'events'

import MyComponent1 from '@/components/MyComponent1.vue'

hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('html', html)

const isActive = ref(true)
const hasError = ref(false)
const error1 = ref<{ type: 'fatal' } | null>(null)

const classObject1 = reactive({
  active: true,
  'text-danger': false
})

const classObject2 = computed(() => ({
  active: isActive.value && !error1.value,
  'text-danger': error1.value && error1.value.type === 'fatal'
}))

const toggelError1 = () => {
  error1.value = error1.value === null ? { type: 'fatal' } : null
}

const highlighted = (code: string, type: string) => {
  return hljs.highlight(code, { language: type }).value.replace(/&amp;lt;/g, '&lt;').replace(/&amp;gt;/g, '&gt;')
}

const rawCode1 = `<div :class="{ active: isActive }">渲染后的效果</div>`
const HTMLElement1 = ref<HTMLElement | null>(null)
const renderingHtml1 = ref<string>("")

const rawCode2 = `const isActive = ref(true)
const hasError = ref(false)`

const rawCode3 = `<span class="static" :class="{ active: isActive, 'text-danger': hasError }">渲染后的效果</span>`
const HTMLElement3 = ref<HTMLElement | null>(null)
const renderingHtml3 = ref<string>("")

const rawCode4 = `// css 样式
.active {
  color: red;
}
.static {
  font-weight: bolder;  
}
.text-danger {
  font-style: italic;
}`

const rawCode5 = `// 脚本
const classObject = reactive({
  active: true,
  'text-danger': false
})`


const rawCode6 = `<!-- 模板 -->
<div :class="classObject">渲染后的效果</div>`
const HTMLElement6 = ref<HTMLElement | null>(null)
const renderingHtml6 = ref<string>("")


const rawCode7 = `// 脚本
const isActive = ref(true)
const error = ref(null)

const classObject = computed(() => ({
  active: isActive.value && !error.value,
  'text-danger': error.value && error.value.type === 'fatal'
}))`

const rawCode8 = `<span :class="classObject">渲染后的效果</span>`
const HTMLElement8 = ref<HTMLElement | null>(null)
const renderingHtml8 = ref<string>("")

const activeClass = ref('active')
const errorClass = ref('text-danger')

const rawCode9 = `// CSS 样式
const activeClass = ref('active')
const errorClass = ref('text-danger')`

const rawCode10 = `<!-- 模板 -->
<span :class="[activeClass, errorClass]">渲染后的效果</span>`
const HTMLElement10 = ref<HTMLElement | null>(null)
const renderingHtml10 = ref<string>("")

const rawCode11 = `<div :class="[isActive ? activeClass : '', errorClass]"></div>`
const HTMLElement11 = ref<HTMLElement | null>(null)
const renderingHtml11 = ref<string>("")

const rawCode12 = `<!-- 使用方括号来创建动态属性名 -->
<div :class="[{ [activeClass]: isActive }, errorClass]"></div>`
const HTMLElement12 = ref<HTMLElement | null>(null)
const renderingHtml12 = ref<string>("")



const rawCode13 = `<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>`

const rawCode14 = `<!-- 在使用组件时 -->
<MyComponent class="baz boo" />`

const rawCode15 = `<p class="foo bar baz boo">Hi!</p>`

const rawCode16 = `<MyComponent :class="{ active: isActive }" />`

const rawCode17 = `<p class="foo bar active">Hi!</p>`

const rawCode18 = `<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>`

const rawCode19 = `<MyComponent class="baz" />`

const rawCode20 = `<p class="baz">Hi!</p>
<span>This is a child component</span>`

const getRenderingHtml = (items: { element: Ref<HTMLElement | null>; result: Ref<string> }[]) => {
  items.forEach(({ element, result }) => {
    nextTick(() => {
      if (element?.value) {
        result.value = element.value.outerHTML
      }
    })
  })
}

const itemsArray = [
  {
    element: HTMLElement1,
    result: renderingHtml1
  },
  {
    element: HTMLElement3,
    result: renderingHtml3
  },
  {
    element: HTMLElement6,
    result: renderingHtml6
  },
  {
    element: HTMLElement8,
    result: renderingHtml8
  },
  {
    element: HTMLElement10,
    result: renderingHtml10
  },
  {
    element: HTMLElement11,
    result: renderingHtml11
  },
  {
    element: HTMLElement12,
    result: renderingHtml12
  }
]

onMounted(() => {
  getRenderingHtml(itemsArray)
})

watch(isActive, () => {
  getRenderingHtml(itemsArray)
})

watch(hasError, () => {
  getRenderingHtml(itemsArray)
})

watch(classObject1, () => {
  getRenderingHtml(itemsArray)
},
  {
    deep: true
  }
)

watch(error1, () => {
  getRenderingHtml(itemsArray)
})
</script>

<style scoped>
p {
  font-size: 16px;
  padding: 2px;
  margin: 5px 0;
}

/* 设置预格式化文本的样式 */
pre {
  /* 背景色 */
  background-color: #f1ffed;
  /* 边框 */
  border: 1px solid #ccc;
  /* 缩小内边距 */
  padding: 5px 5px 5px 10px;
  /* 允许滚动 */
  overflow: auto;
  /* 可选：设置上下外边距 */
  margin: 0px 0;
  /* 允许换行 */
  /* normal: 连续的空白字符会被合并为一个空格。文本会在允许的断点处自动换行。
     nowrap：连续的空白字符会被合并为一个空格。文本不会自动换行，即使在允许的断点处也不会换行。
     pre：保留所有的空白字符（包括空格、制表符和换行符）。文本只会在遇到换行符或者 <br> 标签时换行。
     pre-wrap：保留所有的空白字符。文本会在遇到换行符、<br> 标签，或者达到容器边界时换行。
     pre-line：合并连续的空白字符为一个空格，但保留换行符。文本会在遇到换行符、<br> 标签，或者达到容器边界时换行。
   */
  white-space: pre-wrap;
  /* 行高 */
  line-height: 120%;
  /* 定义缩进 */
  tab-size: 4;
}

h3,
h4,
h5,
h6 {
  margin: 25px 0 10px 0;
  line-height: 100%;
}

.bolder {
  font-weight: bolder;
}

.red {
  color: red;
}

button {
  margin: 5px 5px 5px 10px;
  line-height: 150%;
  width: auto;
  padding: 0 10px;
}

.active {
  color: red;
}

.static {
  font-weight: bolder;
}

.text-danger {
  font-style: italic;
}
</style>
